昨天走完了九面向的前五類(A 風格、B 簡潔、C 安全、D API 慣例、E 架構),也講了為什麼在清單之前,要先放一個「沒有清單」的 subagent。
今天把後四類走完,再補上 CodeGraph 這個結構視角、subagent 的命名規約,以及 finding 進報告前的兩道閘:清單外的發現先提問;沒有驗證過的主張不准掛等級。
昨天那把尺今天收得更準一點:事實多半通用,★ 標的是我們家的處置:判級、豁免、選型。換一家公司,事實還在,處置要重新談。這四類裡有四條掛 ★。
涵蓋以下項目:
dict.get(k, default) 防不了 None。key 存在但值是 None 時,default 不會生效;若該值會沿關鍵路徑傳播並造成資料錯誤或服務失敗,可列為 Critical。Hyrum's Law:
當一個 API 或系統擁有足夠多的使用者時,只看明文契約已經不夠:系統所有可被觀察到的行為,都可能被某個使用者當成依賴。
舉例:
API 的契約只寫著:「驗證失敗時會拋出
ValidationError。」使用者在寫程式碼時,為了判斷細節,直接拿 response body 比對字串:
if (err.message.includes("Invalid ID"))。某天你把錯誤訊息從Invalid ID改成文法更完整的The provided ID is invalid,依賴舊字串的流程控制就會壞掉。比較合適的解法:
- Expand:讓新欄位/新格式與舊版本並存,而不是直接取代。
- Migrate:backfill 存量資料,必要時雙寫以保持兩邊一致。注意:塞一個
server_default或 sentinel 常數不等於完成 backfill;那可能只讓 schema 過得去,資料語意仍然是空的。- Contract:確認所有消費者都搬到新形狀之後,才拆掉舊版本。
延伸閱讀:Hyrum's Law
dict.get(k, default) 防不了 None。這句話寫成一行清單看起來像常識,但它是我們真的摔過才補上的。
情境很單純:key 存在,值是 None。default 只在 key 不存在時才生效,所以它一次都沒有被用上,None 就這樣往下游流。
麻煩的是,修法本身還有第二個陷阱。直覺會改成 dict.get(k) or default,但這個寫法會連 False、0、0.0 一起吃掉,而那些在很多欄位上都是合法值。所以「一律改成 or」也是錯的。
這就是為什麼這條沒辦法寫成機械條文,也沒辦法完全交給 linter。要判斷的是「這個欄位現實上會不會是 None」,以及「它會不會是 0 或 False」;那是情境問題。這一條留在 LLM 這一軌,不在確定性工具那一軌。
重點是斷言品質:驗的是「實際行為」,還是只驗「有回傳東西」?
需要提高警覺的例子:
status_code == 200,卻不檢查 response body 或應有的副作用。assert x is not None,卻沒有驗證內容、狀態變化或業務契約。這些寫法不是一律錯。x is not None 可能正是在守一條契約;mock 也可以用來驗證呼叫參數與互動次數。要抓的是:測試是否只證明了自己安排的結果,卻沒有證明產品行為。
依 diff 涉及的檔案類型,額外啟動前端、容器、Web Server、部署設定與 migration 檢查。Alembic migration 不受「是否包含非 Python 檔」限制;只要 diff 中出現 migration,就必須檢查。
v-html 的 XSS 風險、props 驗證、reactivity 正確性。<script setup>(團隊選型,不是通則)。slim/alpine 是選項,不是通用答案。nginx.conf:確認入口 HTML 的快取/重新驗證策略,並讓帶 content hash 的靜態資產使用長效快取。docker-compose:logging、restart policy、volume 掛載路徑是否符合實際部署需求。修改既有函式時,沿著三個子軸回溯:
「其餘沒有列到的條件,就使用 best practices 進行審查。」
這句是反例 😂。對 AI 說
best practices等於沒說,它不會因為這句話就看得更準。而且要老實講,這句話到現在還留在 skill 裡。
SKILL.md的 Phase 3 寫著:Anything the checklist does not name is reviewed against general best practices。不是忘了改,是我後來想通它拿不掉:清單不可能窮舉,總得有一句交代「清單沒寫到的怎麼辦」,而那句話再怎麼寫都會泛。
能補的是另一半。不去管它「看什麼」,改成管它「能說什麼」:清單沒有涵蓋的發現,一律先以問句浮出,不准直接掛嚴重度。泛的指令留著,但它吐出來的東西不會自動被當成有依據的判定。
九面向是我們願意讓 reviewer 主動下判斷的範圍;清單以外不是禁區,但證據門檻不同。
清單外的發現可以提出,卻不能只靠一句「違反 best practices」就掛上 Critical、Suggestion 或 Nit。先把它寫成問題,說清楚觀察到的現象與需要確認的條件;等驗證完成,才升格成 finding。
第一道閘控制的是:AI 在清單之外能說什麼。
九面向講完了,但清單只交代「要看什麼」,沒有交代「怎麼把散在各處的呼叫關係找齊」。尤其是 I 類的回溯分析,只靠 grep 拼湊,很容易漏掉第二、第三層關係。所以收尾之前,先補一個結構視角的工具。
CLI 在每台執行 review 的機器上安裝一次:
# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh
我的 skill 會先 clone 本次要 review 的 repository,再於該 repository 執行:
cd /path/to/cloned-repository
codegraph init
codegraph init 才是專案層級的初始化:它會建立 .codegraph/,並在同一步完成第一次建圖。因為每次 review 都是在剛 clone 回來的 repository 上執行,所以這一步放在 clone 之後即可。
另外還有一條很像、但責任完全不同的命令:
codegraph install
它不是建索引,而是把 CodeGraph 的 MCP server 與使用說明接進 Claude Code、Codex、Cursor 等 agent。我的 skill 是透過 shell 直接呼叫 CodeGraph CLI,因此這不是每次 review 的必要步驟;只有要讓 agent 直接呼叫 codegraph_explore 等 MCP tool 時,才需要事先在該環境設定一次。
CodeGraph 會掃描 repository,把函式、類別、方法等符號整理成節點,再以 calls、imports、extends、implements 等關係連成圖;部分語言與框架還會有額外的關係解析。
建完之後,原本需要多輪 grep 加上開檔案拼湊的問題,可以改用結構查詢協助回答:
explore:取得相關符號的原始碼、呼叫路徑與影響範圍摘要。callers:誰在呼叫這個函式?也就是 fan-in。impact:修改這個符號,可能波及哪些地方?也就是 blast radius。若走 MCP,現行版本主要透過 codegraph_explore 提供查詢;我的 skill 走 CLI,所以可以直接使用 codegraph callers、codegraph impact 等命令。
不是取代 grep,而是交叉驗證。
CodeGraph 是導航加速器,不是完整性的最終裁決。靜態圖可能受到動態派發、反射、依賴注入、框架慣例、產生碼與索引狀態影響;grep 也可能因搜尋範圍、關鍵字選擇或忽略檔案而漏掉引用。
所以分工規則是:
- 結構性、遞移性的問題,例如「這個改動碰到哪裡」、「誰呼叫這個符號」、「改簽名可能讓誰壞掉」:先用 CodeGraph 建立路徑,再回到實際程式碼驗證。
- 文字層級與枚舉性的問題,例如「設定值在哪些地方出現」、「還有沒有舊名稱」、「是不是少了一個 decorator」:用 grep 補查所有直接命中,再逐一排除誤報。
兩種工具一起使用,可以降低單一工具造成的盲區,但不能保證零誤報或零遺漏。在醫療系統的 review 裡,我們追求的不是宣稱「一定不漏」,而是讓遺漏風險可控,並讓每一條 finding 都有可重現的證據。
這也呼應整套 skill 的工具觀:引入工具之前,先把它「看不見什麼」寫清楚,再決定它能負責什麼。CodeGraph 在我的文件裡,盲區清單比功能清單還長。這不是貶低它,而是能放心使用它的前提。
確定性工具會交給 subagent 執行掃描與匯總。我自己使用的版本指定為 Sonnet,但公開出去的 agent 檔沒有寫死 model:。
這不是硬規則。同一份 skill 不只會在 Claude Code 上執行,Codex 也會使用;把型號寫死,就會讓它綁在特定工具或模型命名上。如果確定只在 Claude Code 裡使用,明確指定模型也沒有問題。
因為後續會追蹤分析流程,所以 subagent 的 subagent_type 採用 ncr-* 格式。沒有安裝這些 agent 檔的環境,skill 會退回 general-purpose subagent,把對應的 agents/ncr-*.md 整份當 prompt 傳入第一行的 ncr-* 標記照樣存在,追蹤不會斷。
我也會在交給 subagent 的 prompt 第一行放上對應的 ncr-* 標記。
ncr是nathan-code-review的縮寫。未來分析 Claude Code 的 JSONL transcript 時,可以直接 grep 這個模式,定位各個 subagent 的進度與當下程序。
AI reviewer 最常見的錯誤型態,是自信滿滿地宣稱「這裡少了驗證」或「這段被刪掉會壞」,但那段邏輯其實只是搬到了同一個 diff 的別處。
所以我立了一條硬規則:任何宣稱「缺了什麼」或「某條路徑會壞」的 finding,寫進報告前都必須先找過反證:
找過反證後仍然成立,才能掛上 Critical、Suggestion 或 Nit 進入報告。無法驗證的內容,不准掛等級,只能以「提問」形式浮出,並明確標示尚未驗證。
第一道閘管清單外的發現,第二道閘管所有 finding 的證據。兩道閘守的是同一件事:不要讓 AI 的語氣比證據更有自信。
誤報真正殺傷的是信任。 報告只要被抓到一次自信的錯,後面的 Critical 就不會再有人認真看了。
九個面向到今天走完了。
回頭看,這份清單真正花時間的,不是「列出檢查項目」,而是替每一項標出嚴重度、寫下豁免情境,以及決定衝突時誰優先。那些才是「像我」的部分;清單本身反而最容易抄。
中間的 CodeGraph 也不是單純「多一個工具」,而是示範了引入工具的順序:先寫清楚它看不見什麼,再決定它能負責什麼。
最後再用兩道閘把輸出收住:清單外的發現先提問;所有 finding 都必須驗證並找過反證,才能掛上等級。
Day 1 那張表裡的「以正合,以奇勝」,到今天兩邊才算到齊:正是 Day 5 那些確定性工具,奇是這兩天的九面向。
明天進入最後一段規劃:報告要長什麼樣、怎麼交付、怎麼發佈出去。